﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.Remoting;
using System.Text;

namespace CSharpExt.Reflection
{
    /// <summary>
    /// ---------------------------------------------------------------------
    /// - AppDomain
    /// ---------------------------------------------------------------------
    /// CreateInstance(String, String) -> ObjectHandle
    ///     Creates a new instance of the specified type defined in the specified assembly. 
    /// 
    /// CreateInstance(String, String, Object[]) -> ObjectHandle
    ///     Creates a new instance of the specified type defined in the specified assembly. 
    ///     A parameter specifies an array of activation attributes.
    /// 
    /// CreateInstance(String, String, Boolean, BindingFlags, Binder, Object[], CultureInfo, Object[]) -> ObjectHandle 
    ///     Creates a new instance of the specified type defined in the specified assembly. 
    ///     Parameters specify a binder, binding flags, constructor arguments, culture-specific 
    ///     information used to interpret arguments, and optional activation attributes. 
    /// 
    /// CreateInstanceAndUnwrap(String, String) -> Object
    ///     Creates a new instance of the specified type. Parameters specify the assembly 
    ///     where the type is defined, and the name of the type. 
    /// 
    /// CreateInstanceAndUnwrap(String, String, Object[]) -> Object
    ///     Creates a new instance of the specified type. Parameters specify the assembly 
    ///     where the type is defined, the name of the type, and an array of activation attributes. 
    /// 
    /// CreateInstanceAndUnwrap(String, String, Boolean, BindingFlags, Binder, Object[], CultureInfo, Object[]) -> Object
    ///     Creates a new instance of the specified type defined in the specified assembly, 
    ///     specifying whether the case of the type name is ignored; the binding attributes 
    ///     and the binder that are used to select the type to be created; the arguments of 
    ///     the constructor; the culture; and the activation attributes.
    /// 
    /// CreateInstanceFrom(String, String) -> ObjectHandle
    ///     Creates a new instance of the specified type defined in the specified assembly file. 
    /// 
    /// CreateInstanceFrom(String, String, Object[]) -> ObjectHandle
    ///     Creates a new instance of the specified type defined in the specified assembly file. 
    /// 
    /// CreateInstanceFrom(String, String, Boolean, BindingFlags, Binder, Object[], CultureInfo, Object[]) -> ObjectHandle
    ///     Creates a new instance of the specified type defined in the specified assembly file. 
    /// 
    /// CreateInstanceFromAndUnwrap(String, String) -> Object
    ///     Creates a new instance of the specified type defined in the specified assembly file. 
    /// 
    /// CreateInstanceFromAndUnwrap(String, String, Object[]) -> Object
    ///     Creates a new instance of the specified type defined in the specified assembly file. 
    /// 
    /// CreateInstanceFromAndUnwrap(String, String, Boolean, BindingFlags, Binder, Object[], CultureInfo, Object[]) -> Object
    ///     Creates a new instance of the specified type defined in the specified assembly file, 
    ///     specifying whether the case of the type name is ignored; the binding attributes and 
    ///     the binder that are used to select the type to be created; the arguments of the 
    ///     constructor; the culture; and the activation attributes. 
    /// 
    /// ---------------------------------------------------------------------
    /// - Activator
    /// ---------------------------------------------------------------------
    /// CreateInstance(Type) -> Object
    ///     Creates an instance of the specified type using that type's default constructor.
    /// 
    /// CreateInstance(Type, Boolean nonPublic) -> Object
    ///     Creates an instance of the specified type using that type's default constructor.
    /// 
    /// CreateInstance(Type, Object[]) -> Object
    ///     Creates an instance of the specified type using the constructor that best matches 
    ///     the specified parameters.
    /// 
    /// CreateInstance(Type, Object[], Object[]) -> Object
    ///     Creates an instance of the specified type using the constructor that best matches 
    ///     the specified parameters.
    /// 
    /// CreateInstance(Type, BindingFlags, Binder, Object[], CultureInfo) -> Object
    ///     Creates an instance of the specified type using the constructor that best matches 
    ///     the specified parameters.
    /// 
    /// CreateInstance(Type, BindingFlags, Binder, Object[], CultureInfo, Object[]) -> Object
    ///     Creates an instance of the specified type using the constructor that best matches 
    ///     the specified parameters.
    /// 
    /// 
    /// CreateInstance(AppDomain, String, String) -> ObjectHandle
    ///     Creates an instance of the type whose name is specified in the specified remote 
    ///     domain, using the named assembly and default constructor.
    /// 
    /// CreateInstance(AppDomain, String, String, Boolean, BindingFlags, Binder, Object[], CultureInfo, Object[]) -> ObjectHandle
    ///     Creates an instance of the type whose name is specified in the specified remote 
    ///     domain, using the named assembly and the constructor that best matches the specified 
    ///     parameters.
    /// 
    /// 
    /// CreateInstance(String, String) -> ObjectHandle
    ///     Creates an instance of the type whose name is specified, using the named assembly 
    ///     and default constructor.
    /// 
    /// CreateInstance(String, String, Object[]) -> ObjectHandle
    ///     Creates an instance of the type whose name is specified, using the named assembly 
    ///     and default constructor.
    /// 
    /// CreateInstance(String, String, Boolean, BindingFlags, Binder, Object[], CultureInfo, Object[]) -> ObjectHandle
    ///     Creates an instance of the type whose name is specified, using the named assembly 
    ///     and the constructor that best matches the specified parameters.
    /// 
    /// 
    /// CreateInstance<T>() -> T
    ///     Creates an instance of the type designated by the specified generic type parameter, 
    ///     using the parameterless constructor .
    /// 
    /// 
    /// CreateInstanceFrom(String, String) -> ObjectHandle
    ///     Creates an instance of the type whose name is specified, using the named assembly 
    ///     file and default constructor.
    /// 
    /// CreateInstanceFrom(String, String, Object[]) -> ObjectHandle
    ///     Creates an instance of the type whose name is specified, using the named assembly 
    ///     file and default constructor.
    /// 
    /// CreateInstanceFrom(String, String, Boolean, BindingFlags, Binder, Object[], CultureInfo, Object[]) -> ObjectHandle
    ///     Creates an instance of the type whose name is specified, using the named assembly 
    ///     file and the constructor that best matches the specified parameters.
    /// 
    /// 
    /// CreateInstanceFrom(AppDomain, String, String) -> ObjectHandle
    ///     Creates an instance of the type whose name is specified in the specified remote 
    ///     domain, using the named assembly file and default constructor.
    /// 
    /// CreateInstanceFrom(AppDomain, String, String, Boolean, BindingFlags, Binder, Object[], CultureInfo, Object[]) -> ObjectHandle
    ///     Creates an instance of the type whose name is specified in the specified remote 
    ///     domain, using the named assembly file and the constructor that best matches the 
    ///     specified parameters.
    /// 
    /// 
    /// ---------------------------------------------------------------------
    /// - TypeActivator
    /// ---------------------------------------------------------------------
    /// 
    /// Permette di creare un'istanza di un tipo indicato come stringa.
    /// L'utilizzo e' il seguente:
    /// 
    ///     TypeActivator.CreateInstance<interface>("explicitType")
    /// 
    /// Ovviamente "explicitType" deve implementare l'interfaccia indicata.
    /// 
    /// NON serve indicare l'assembly in cui il tipo e' contenuto, lo cerca in automatico
    /// nel seguente modo:
    /// 
    ///     1) tra gli assembly correntemente in memoria
    ///     2) tra gli assembly presenti nella AppDomain.BaseDirectory
    ///     3) tra gli assembly dell'installazione del framework .NET
    /// 
    /// </summary>
    public static class TypeActivator
    {
        #region CreateInstance

        public static T CreateInstance<T>(String typeName)
        {
            return CreateInstance<T>(typeName, null);
        }

        public static T CreateInstance<T>(String typeName, params Object[] args)
        {
            return (T)CreateInstanceAndUnwrap(typeName, args);
        }

        #endregion

        #region LoadType

        /// <summary>
        /// Permette di caricare un tipo generico.
        /// </summary>
        public static Type LoadType(String typeName, Type[] typeArgs)
        {
            String genericName = String.Format("{0}`{1}", typeName, typeArgs.Length);
            Type type = LoadType(genericName);
            return type.MakeGenericType(typeArgs);
        }

        public static Type LoadType(String typeName)
        {
            Type type;
            
            typeName = NormalizeType(typeName);

            type = loadTypeUsingLoadedAssemblies(typeName);
            if (type != null)
                return type;

            type = loadTypeUsingAssembliesInDirectory(AppDomain.CurrentDomain.BaseDirectory, typeName);
            if (type != null)
                return type;

            type = loadTypeUsingAssembliesInDirectory(FrameworkDirectory, typeName);
            if (type != null)
                return type;

            throw new TypeLoadException(String.Format("Could not load type '{0}'", typeName));
        }

        #endregion

        #region CreateInstanceAndUnwrap

        public static Object CreateInstanceAndUnwrap(String typeName)
        {
            return CreateInstanceAndUnwrap(typeName, null);
        }
        
        public static Object CreateInstanceAndUnwrap(String typeName, params Object[] args)
        {
            Type type = LoadType(typeName);
            
            return Activator.CreateInstance(type, args);
        }

        #endregion

        #region Support

        public static void DumpAssemblies()
        {
            Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies();
            foreach (Assembly assembly in assemblies)
            {
                dumpAssemblyInfo(assembly);
            }
        }

        public static void DumpTypes()
        {
            DumpTypes("");
        }

        public static void DumpTypes(String prefix)
        {
            Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies();
            foreach (Assembly assembly in assemblies)
            {
                bool found = false;
                foreach (Type type in assembly.GetTypes())
                {
                    String fullName = type.FullName;
                    if (!fullName.StartsWith(prefix))
                        continue;

                    if (!found)
                        dumpAssemblyInfo(assembly);

                    found = true;
                    Console.WriteLine("    Type '{0}'", type.FullName);
                }
                if (found)
                    Console.WriteLine("End");
            }
        }

        #endregion

        #region Implementation

        private static void dumpAssemblyInfo(Assembly assembly)
        {
            Console.WriteLine("Assembly '{0}' [{1}]", assembly.GetName().Name, assembly.Location);
        }

        
        private static Type loadTypeUsingLoadedAssemblies(String typeName)
        {
            Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies();
            foreach (Assembly assembly in assemblies)
            {
                Type ctype = assembly.GetType(typeName);
                if (ctype == null)
                    continue;

                return ctype;
            }

            return null;
        }

        private static Type loadTypeUsingAssembliesInDirectory(String directory, String typeName)
        {
            AppDomain analyzingDomain = AppDomain.CreateDomain("AnalyzingDomain");
            analyzingDomain.ReflectionOnlyAssemblyResolve += reflectionOnlyAssemblyResolve;
            analyzingDomain.SetData("TypeName", typeName);
            analyzingDomain.SetData("AssemblyFile", null);
            analyzingDomain.SetData("Directory", directory);

            try
            {
                analyzingDomain.DoCallBack(findAssemblyFile);
                String assemblyFile = (String)analyzingDomain.GetData("AssemblyFile");
                if (assemblyFile != null)
                {
                    Assembly assembly = Assembly.LoadFrom(assemblyFile);
                    return assembly.GetType(typeName);
                }
                return null;
            }
            finally
            {
                analyzingDomain.ReflectionOnlyAssemblyResolve -= reflectionOnlyAssemblyResolve;
                AppDomain.Unload(analyzingDomain);
            }
        }


        private static Object createUsingLoadedAssemblies(String typeName, Object[] args)
        {
            Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies();
            foreach (Assembly assembly in assemblies)
            {
                Type ctype = assembly.GetType(typeName);
                if (ctype == null)
                    continue;

                return assembly.CreateInstance(typeName, false, BindingFlags.CreateInstance, null, args, null, null);
            }

            return null;
        }

        private static Object createUsingAssembliesInDirectory(String directory, String typeName, Object[] args)
        {
            AppDomain analyzingDomain = AppDomain.CreateDomain("AnalyzingDomain");
            analyzingDomain.ReflectionOnlyAssemblyResolve += reflectionOnlyAssemblyResolve;
            analyzingDomain.SetData("TypeName", typeName);
            analyzingDomain.SetData("AssemblyFile", null);
            analyzingDomain.SetData("Directory", directory);

            try
            {
                analyzingDomain.DoCallBack(findAssemblyFile);
                String assemblyFile = (String) analyzingDomain.GetData("AssemblyFile");
                if (assemblyFile != null)
                {
                    ObjectHandle oh = Activator.CreateInstanceFrom(
                        assemblyFile, 
                        typeName, 
                        false, 
                        BindingFlags.CreateInstance, 
                        null, 
                        args, 
                        null, 
                        null);

                    return oh.Unwrap();
                }
                return null;
            }
            finally
            {
                analyzingDomain.ReflectionOnlyAssemblyResolve -= reflectionOnlyAssemblyResolve;
                AppDomain.Unload(analyzingDomain);
            }
        }

        
        private static void findAssemblyFile()
        {
            String typeName = (String)AppDomain.CurrentDomain.GetData("TypeName");
            String baseDirectory = (String)AppDomain.CurrentDomain.GetData("Directory");
            
            DirectoryInfo di = new DirectoryInfo(baseDirectory);
            FileInfo[] files = di.GetFiles("*.dll");
            foreach (FileInfo fi in files)
            {
                try
                {
                    Assembly assembly = Assembly.ReflectionOnlyLoadFrom(fi.FullName);
                    Type ctype = assembly.GetType(typeName);
                    if (ctype != null)
                    {
                        AppDomain.CurrentDomain.SetData("AssemblyFile", fi.FullName);
                        break;
                    }
                }
                catch (Exception)
                {
                    
                }
            }
        }

        private static Assembly reflectionOnlyAssemblyResolve(Object sender, ResolveEventArgs args)
        {
            AssemblyName assemblyName = new AssemblyName(args.Name);

            String baseDirectory = AppDomain.CurrentDomain.BaseDirectory;
            String path = Path.Combine(baseDirectory, assemblyName.Name + ".dll");
            if (File.Exists(path))
            {
                // e' un assembly 'custom'
                try
                {
                    return Assembly.ReflectionOnlyLoadFrom(path);
                }
                catch (Exception)
                {
                    return null;
                }
            }
            else
            {
                // forse e' un assembly di sistema
                try
                {
                    return Assembly.ReflectionOnlyLoad(args.Name);
                }
                catch (Exception)
                {
                    
                }
            }

            // Nota: il nome dell'assembly NON HA NESSUNA RELAZIONE con il nome del file che lo contiene, 
            // cioe' l'assembly si puo' chiamare "pippo" e il file "pluto.dll".
            // Questo perche' il nome dell'assembly (versione, cultura, ...) e' un'informazione contenuta 
            // nel file manifest. 
            // Quindi, prima di perdere ogni speranza, conviene fare ancora un tentativo, scandire tutte le
            // DLL della directory, e per ogn'una di queste controllare il nome dell'assembly 
            foreach (String file in Directory.GetFiles(baseDirectory, "*.dll"))
            {
                String assemblyFile = Path.Combine(baseDirectory, file);
                try
                {
                    AssemblyName thisAssemblyName = AssemblyName.GetAssemblyName(assemblyFile);
                    if (thisAssemblyName.FullName.Equals(assemblyName.FullName))
                        return Assembly.ReflectionOnlyLoad(assemblyFile);
                }
                catch (Exception)
                {
                    // la presenza di errori non e' significativa in questo contesto
                    // (o la va, o la spacca)!
                }
            }

            return null;
        }

        private static String FrameworkDirectory
        {
            get { return Path.GetDirectoryName(typeof(Environment).Assembly.Location); }
            
//            get 
//            {
//                String directory = Environment.GetFolderPath(Environment.SpecialFolder.Windows);
//                Version version = Environment.Version;
//                
//                if(Environment.Is64BitProcess)
//                {
//                    directory = Path.Combine(directory, "Microsoft.NET\\Framework64");
//                }
//                else
//                {
//                    directory = Path.Combine(directory, "Microsoft.NET\\Framework");
//                }
//                
//                if (version.Major == 3)
//                {
//                    directory = Path.Combine(directory, String.Format("v{0}.{1}", version.Major, version.Minor));
//                }
//                else
//                {
//                    directory = Path.Combine(directory, String.Format("v{0}.{1}.{2}", version.Major, version.Minor, version.Build));
//                }
//                
//                if (!Directory.Exists(directory))
//                    throw new IOException("Unable to retrieve .NET Framework installation directory");
//                
//                return directory;
//            }
        }
        
        #endregion

        #region Predefined Types
        
        public static String NormalizeType(String typeName)
        {
            if (DEFAULT_TYPES.ContainsKey(typeName))
                return DEFAULT_TYPES[typeName];
            else
                return typeName;
        }
        

        private static Dictionary<String, String> DEFAULT_TYPES =
            new Dictionary<String, String>()
                {
                    {"char",    "System.Char"}, 
                    
                    {"byte",    "System.Byte"}, 
                    {"short",   "System.Int8"}, 
                    {"int",     "System.Int32"}, 
                    {"long",    "System.Int64"}, 

                    {"sbyte",   "System.SByte"}, 
                    {"ushort",  "System.UInt8"}, 
                    {"uint",    "System.UInt32"}, 
                    {"ulong",   "System.UInt64"}, 

                    {"float",   "System.Single"}, 
                    {"double",  "System.Double"}, 
                    {"string",  "System.String" }, 
                    {"String",  "System.String"}, 

                    {"decimal", "System.Decimal"}, 
                    {"Decimal", "System.Decimal"}, 
                    {"object",  "System.Object"}, 
                    {"Object",  "System.Object"}, 
                };

        #endregion
    }
}
